/*
 * Copyright (C) 2011 ARNDroid
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package br.com.arndroid.lib.entity;

import java.util.Iterator;
import java.util.List;

import br.com.arndroid.lib.validation.ConstraintViolations;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup.LayoutParams;


/**
 * A form for data collect validation and persistence of a entity.
 *
 * <p>
 * This class is responsible for interation with the user in a defined interface provided
 * for you and have hook methods for customizations of layout, validation (with show
 * of message erros in i18n) and persistence.
 * </p>
 *
 * <p>
 * There are <a href="#Preconditions">preconditions</a> for use of this class.
 * </p>
 *
 * <p>
 * See the <a href="#TipicalUse">tipical use</a> and the <a href="#LifeCycle">life cycle</a> of the class.
 * </p>
 *
 * <a name="Preconditions"></a>
 * <h4>Preconditions</h4>
 * <p>
 * You must have a {@link EntityPOJO} subclass and a xml layout for the dialog (with positive and negative buttons).
 * </p>
 *
 * <a name="TipicalUse"></a>
 * <h4>Tipical Use</h4>
 * <p>
 * For layout definition, overrides in your subclass:
 * <ol>
 *   <li>{@link #onAcquireLayoutResID} to provide the resource id for the layout of activity (wich must contains the positive and negative buttons);</li>
 *   <li>{@link #onAcquireOperationTitle(String)} to provide a title dependents on operation (action) string passed;</li>
 *   <li>{@link #onAcquirePositiveButtonResID} to provide the resource id for a ok like button;</li>
 *   <li>{@link #onAcquireNegativeButtonResID} to provide the resource id for a cancel like button;</li>
 *   <li>{@link #onAcquireValidationErrorTitle} to provide the string for the title in validation errors;</li>
 *   <li>{@link #onAcquireValidationErrorMessagePrefix} to provide the string prefix (bullet) for show in validation errors;</li>
 *   <li>{@link #onBindUI} to make binds your code variables from your widgets of layout;</li>
 *   <li>{@link #onInitUI} to enter default values in the widgets.</li>
 * </ol>
 * </p>
 * <p>
 * For validation definition, overrides in your subclass:
 * <ol>
 *   <li>{@link #onTranslateErrorMessage} to i18n of yours errors messages;</li>
 *   <li>{@link #onValidateUI} to validation of fields;</li>
 *   <li>if necessary {@link #isPersistable}.</li>
 * </ol>
 * </p>
 * <p>
 * For persistence definition, overrides in your subclass:
 * <ol>
 *   <li>{@link #onPersistUI} to persistence of fields.</li>
 * </ol>
 * </p>
 * <p>
 * To call the form:
 * <pre>
 * {@code
 *  Intent myIntent = new Intent(yourContext, EntityFormActivitySubclass.class);
 * }
 * </pre>
 * or:
 * <pre>
 * {@code
 *  myIntent.setClassName(yourContext, EntityFormActivitySubclass.class);
 * }
 * </pre>
 * and for new operations:
 * </pre>
 * {@code
 *  myIntent.setAction(EntityManagerActivity.ACTION_ENTITY_NEW);
 * }
 * </pre>
 * or for update operations:
 * <pre>
 * {@code
 *  myIntent.setAction(EntityManagerActivity.ACTION_ENTITY_UPDATE);
 *  myIntent.putExtra(EntityManagerActivity.EXTRA_ENTITY_ID, idForUpdate);
 * }
 * </pre> 
 * </p>
 * <p>
 * The result code and intent return are detailed in operational life cycle below.
 * </p>
 * <a name="LifeCycle"></a>
 * <h4>Life Cycle</h4>
 * <p>
 * There are two cycles: creational and operational.
 * </p>
 * <p>
 * <b>Creational Cycle</b>
 * </p>
 * <p>
 * In order are call yours overrides of:
 * <ol>
 * 	<li>{@link #onAcquireLayoutResID};</li>
 * 	<li>{@link #onAcquireOperationTitle};</li>
 * 	<li>{@link #onAcquirePositiveButtonResID};</li>
 * 	<li>{@link #onAcquireNegativeButtonResID};</li>
 * 	<li>{@link #onBindUI};</li>
 * 	<li>and if is not a screen restore, {@link #onInitUI}.</li>
 * </ol>
 * </p>
 * <p>
 * <b>Operational Cycle</b>
 * </p>
 * <p>
 * The follow mapping is the cycle description:
 * <pre>
 * {@code
 * Request/validation/return mapping:
 * ==================================
 * For new entity:
 *   |If user click in the negative button (Cancel):
 *     |EntityManagerActivity returns to caller:
 *       |-> Result code == RESULT_CANCELED,
 *       |-> A null intent.
 *   |If user click in the positive button (OK):
 *     |Your EntityManagerActivity.validateUI(String) overrides is call.
 *     |If EntityManagerActivity.validate(String) return a ConstraintViolations:
 *       |The ConstraitViolations.formatedErrorMessage() is show to the user
 *     |If EntityManagerActivity.validateUI(String) return null:
 *       |Your EntityManagerActivity.isPersistable(String) overrides is call.
 *       |If EntityManagerActivity.isPersistable(String) return false:
 *         |(new entity not persistable: this scenario may be rare but is valid...)
 *         |EntityManagerActivity returns to caller:
 *           |-> Result code == RESULT_OK,
 *           |-> A intent with EXTRA_ENTITY_ID == ENTITY_ID_UNDEFINED_VALUE and EXTRA_ENTITY_PERSISTABLE == false
 *       |If EntityManagerActivity.isPersistable(String) return true:
 *         |EntityManagerActivity.persistUI(String) is call and return "the new id".
 *         |EntityManagerActivity returns to caller:
 *           |-> Result code == RESULT_OK,
 *           |-> A intent with EXTRA_ENTITY_ID == "the new id" and EXTRA_ENTITY_PERSISTABLE == true
 * 
 * For update entity:
 *   |If user click in the negative button (Cancel):
 *     |EntityManagerActivity returns to caller:
 *       |-> Result code == RESULT_CANCELED,
 *       |-> A null intent.
 *   |If user click in the positive button (OK):
 *     |Your EntityManagerActivity.validateUI(String) overrides is call.
 *     |If EntityManagerActivity.validateUI(String) return a ConstraintViolations:
 *       |The ConstraintViolations.formatedErrorMessage() is show to the user
 *     |If EntityManagerActivity.validateUI(String) return null:
 *       |Your EntityManagerActivity.isPersistable(String) overrides is call.
 *       |If EntityManagerActivity.isPersistable(String) return false:
 *         |(update entity not persistable - may be none data modified...)
 *         |EntityManagerActivity returns to caller:
 *           |-> Result code == RESULT_OK,
 *           |-> A intent with EXTRA_ENTITY_ID == "the id for update" and EXTRA_ENTITY_PERSISTABLE == false
 *       |If EntityManagerActivity.isPersistable(String) return true:
 *         |EntityManagerActivity.persistUI(String) is call and return "the new updated id" (normally == "the id for update").
 *         |EntityManagerActivity returns to caller:
 *           |-> Result code == RESULT_OK,
 *           |-> A intent with EXTRA_ENTITY_ID == "the new updated id" and EXTRA_ENTITY_PERSISTABLE == true
 * }
 * </pre>
 * </p>
 *
 * @author Alceu Rodrigues Neto
 */
public abstract class EntityFormActivity extends Activity {
	
	// DATA MEMBERS
	
	// ID in operation (if new operation, null):
	private long mID = ENTITY_ID_UNDEFINED_VALUE;

	private String mErrorMessageToShow;
	
	// CONSTANTS
	
	/**
	 * Denotes entity creation action. 
	 */
	public static final String ENTITY_ACTION_NEW = "br.com.arndroid.intent.ACTION_NEW";
	
	/**
	 * Denotes entity update action. 
	 */
	public static final String ENTITY_ACTION_UPDATE = "br.com.arndroid.intent.ACTION_UPDATE";
	
	/**
	 * Representation of a ID in a bundle. 
	 */
	public static final String EXTRA_ENTITY_ID = "br.com.arndroid.lib.intent.EXTRA_ID";
	
	/**
	 * Representation in a bundle if the last operation was persisted. 
	 */
	public static final String EXTRA_ENTITY_PERSISTABLE = "br.com.arndroid.lib.intent.EXTRA_PERSISTABLE";
	
	/**
	 * Special value of a undefined ID. 
	 */
	public static final long ENTITY_ID_UNDEFINED_VALUE = -2L;
	
	/**
	 * Special value of a new ID. 
	 */
	public static final long ENTITY_ID_NEW_VALUE = -1L;
	
	// Dialogs constants:
	private static final int DIALOG_ID_ERROR = 1;

	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // Internal state initialization:
        if(savedInstanceState == null) {
        	// This is a new call, get the intent:
        	Intent intent = getIntent();
        	if(ENTITY_ACTION_NEW.equals(intent.getAction())) {
        		mID = ENTITY_ID_NEW_VALUE;
        	} else if(ENTITY_ACTION_UPDATE.equals(intent.getAction())) {
        		mID = intent.getLongExtra(EXTRA_ENTITY_ID, ENTITY_ID_UNDEFINED_VALUE);
                if(mID == ENTITY_ID_UNDEFINED_VALUE || mID == ENTITY_ID_NEW_VALUE)
                	throw new IllegalArgumentException("Invalid Intent.Extra.ENTITY_MANAGER_ID");
        	} else {
        		throw new IllegalArgumentException("Invalid Intent.Action");
        	}
        	
        } else {
        	mID = savedInstanceState.getLong(EXTRA_ENTITY_ID, ENTITY_ID_UNDEFINED_VALUE);
            // If m_id undefined, a internal error found.
            if(mID == ENTITY_ID_UNDEFINED_VALUE)
            	throw new IllegalStateException("EntityManager m_id invalid");
        }

        // Inflate and set a user defined layout:
        setContentView(onAcquireLayoutResID());
        getWindow().setLayout(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT);
        
        // Set the title:
        setTitle(onAcquireOperationTitle(getCurrentAction()));
        
        // Set positive button listener:
        findViewById(onAcquirePositiveButtonResID()).setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
            	performPositiveAction();
            }
        });
        
        // Set negative button listener:
        findViewById(onAcquireNegativeButtonResID()).setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
            	performNegativeAction();
            }
        });

	        
        // UI binds:
        onBindUI();
        
        // UI initializations, if necessary:
        if(savedInstanceState == null)
        	onInitUI(getCurrentAction());
    }
    
    @Override
    protected void onSaveInstanceState(Bundle outState) {
    	super.onSaveInstanceState(outState);
    	outState.putLong(EXTRA_ENTITY_ID, mID);
    }
    
	// HOOKS
    
	/**
	 * Return the id of your resource layout.
	 * 
	 * @return the id of your resource layout
	 */
	public abstract int onAcquireLayoutResID();
	
	/**
	 * Return the title current operation show in dialog title.
	 * 
	 * @param currentAction {@link #ENTITY_ACTION_NEW} or {@link #ENTITY_ACTION_UPDATE} 
	 * @return	the title to be show
	 */
	public abstract String onAcquireOperationTitle(String currentAction);
	
	/**
	 * Return the id of your positive (OK) button.
	 * 
	 * @return the id of your positive button
	 */
	public abstract int onAcquirePositiveButtonResID();
	
	/**
	 * Return the id of your negative (Cancel) button.
	 * 
	 * @return the id of your negative button
	 */
	public abstract int onAcquireNegativeButtonResID();
	
	/**
	 * Return the title of validation error dialog wich is show
	 * in ConstraintViolations ocurrence.
	 * 
	 * @return the title of validation error dialog
	 */
	public abstract String onAcquireValidationErrorTitle();
	
	/**
	 * Return the characters wich is your line of error prefix (bullet).
	 * 
	 * @return the line of error prefix characters
	 */
	public abstract String onAcquireValidationErrorMessagePrefix();
	
	/**
	 * Hook for bind on user interface. This is the ideal point to initilize
	 * members of class (inded of new onCreate override) and bind of ui elements.
	 */
	public abstract void onBindUI();
	
	/**
	 * This method is call if and only if is <i>not</i> a restore screen.
	 * Use this for user interface inital defaults, eg in a update operation
	 * the interface fields may be filled with the actual content in database.
	 * 
	 * @param currentAction {@link #ENTITY_ACTION_NEW} or {@link #ENTITY_ACTION_UPDATE}
	 */
	public abstract void onInitUI(String currentAction);
	
	/**
	 * Validate the user entered values here. Return null if validated,
	 * otherwise a initialized ConstraintViolations.
	 * 
	 * @param currentAction {@link #ENTITY_ACTION_NEW} or {@link #ENTITY_ACTION_UPDATE}
	 * @return	a initialized ConstraintViolations or null (if there are no violations)
	 */
	public abstract ConstraintViolations onValidateUI(String currentAction);
	
	/**
	 * Make the user interface persistence here and return the persisted id.
	 * 
	 * @param currentAction {@link #ENTITY_ACTION_NEW} or {@link #ENTITY_ACTION_UPDATE}
	 * @return	the id for persisted entity
	 */
	public abstract long onPersistUI(String currentAction);
	
	/**
	 * A hook for avail of return to be executed. A common use is for
	 * tests of return values. The default implamentation do nothing.
	 * 
	 * @param resultCode	the result code to be returned to the caller
	 * @param resultIntent	the intent to be returned to the caller
	 */
	public void onFinish(int resultCode, Intent resultIntent) {}
	
	/**
	 * Translate errors here. The implamentation must put here localized code.
	 * 
	 * @param errorMessage the error message to be translated
	 * @return	the translated error message
	 */
	public abstract String onTranslateErrorMessage(String errorMessage);
	
	
	// ACTIONS:
	// Positive action:
	protected void performPositiveAction() {
		String currentAction = getCurrentAction();
		ConstraintViolations violations;
		Intent result;
		
		// Validate user entry?
		violations = onValidateUI(currentAction); 
		if(violations == null) {
			result = new Intent();
			if(isPersistable(currentAction)) {
				result.putExtra(EXTRA_ENTITY_PERSISTABLE, true);
				result.putExtra(EXTRA_ENTITY_ID, onPersistUI(currentAction));
			}
			else {
				result.putExtra(EXTRA_ENTITY_PERSISTABLE, false);
				result.putExtra(EXTRA_ENTITY_ID,
						ENTITY_ACTION_NEW.equals(getCurrentAction()) ? ENTITY_ID_UNDEFINED_VALUE : mID);
			}
			doFinish(RESULT_OK, result);
		} else {
			// Entry not valid: show error message:
			showErrorMessage(violations);
		}
	}

	// Negative action:
	protected void performNegativeAction() {
		// Operation canceled:
        doFinish(RESULT_CANCELED, null);
	}
	
	@Override
	protected Dialog onCreateDialog(int id) {
		switch (id) {
		case DIALOG_ID_ERROR:
			return new AlertDialog.Builder(this)
			.setTitle(onAcquireValidationErrorTitle())
			// Hack - If not set, the message customization in onPrepareDialog(id, dialog) fails:
			.setMessage("")
			.setPositiveButton(android.R.string.ok, null)
			.create();
		default:
			throw new IllegalArgumentException("Invalid id inside EntityManagerActivity.onCreateDialog(id)");
		}
	}
	
	@Override
	protected void onPrepareDialog(int id, Dialog dialog) {
		switch (id) {
		case DIALOG_ID_ERROR:
			((AlertDialog)dialog).setMessage(mErrorMessageToShow);
			break;
		default:
			throw new IllegalArgumentException("Invalid id inside EntityManagerActivity.onPrepareDialog(id, dialog)");
		}
	}
	
	// UTILITIES:
	// Entity persistence evaluation (may be override for customized evaluation):
	public boolean isPersistable(String currentAction) {
		// Default implementation:
		// If valid (this call is set if, and only if, isValid == true), is persistable
		return true;
	}

	// Return in course action:
	protected String getCurrentAction() {
		return mID == ENTITY_ID_NEW_VALUE ? ENTITY_ACTION_NEW : ENTITY_ACTION_UPDATE;
	}
	
	// Return entity id:
	protected long getEntityID() {
		return mID;
	}

	// Format (and translate) error message for show:
	protected String formatErrorMessage(ConstraintViolations violations) {
		StringBuilder result = new StringBuilder("");
		List<String> listErrors = violations.getViolationsList();
		
		if(listErrors.size() > 0) {
			Iterator<String> iter = listErrors.iterator();
			// First element iteration:
			result.append(onAcquireValidationErrorMessagePrefix()).
			       append(onTranslateErrorMessage(iter.next()));
			// Other elements iteration if necessary:
			while(iter.hasNext()) {
				result.append('\n').
					   append(onAcquireValidationErrorMessagePrefix()).
					   append(onTranslateErrorMessage(iter.next()));
			}
		}
		
		return result.toString();
	}
	
	// Error message show (may be override for customized presentation):
	public void showErrorMessage(ConstraintViolations violations) {
		mErrorMessageToShow = formatErrorMessage(violations);
		showDialog(DIALOG_ID_ERROR);
	}
	
	// Do finalizations with method hock for tests:
	protected void doFinish(int resultCode, Intent resultIntent) {
		onFinish(resultCode, resultIntent);
		setResult(resultCode, resultIntent);
		finish();
	}
	

}
